package ru.emms.codemeter;

import com.intellij.openapi.diagnostic.Logger;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import ru.emms.codemeter.configuration.Configuration;
import ru.emms.codemeter.configuration.ConfigurationChangeListener;
import ru.emms.codemeter.metrics.Metric;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Created by IntelliJ IDEA.
 * User: nastya.kuz
 * Date: 12.08.2010
 * Time: 14:41:51
 * Работа с базой данных, в которой плагин хранит рассчитанные метрики.
 */
public class DataBaseManager {

    private Logger log = Logger.getInstance(getClass().getName());
    private QueryRunner queryRunner;
    private BeanListHandler<Metric> beanListHandler;
    private Configuration configuration;
    //при выборке по '*' мы не получаем SQLException в случае отсутствия столбца, поэтому надо указывать столбцы явно
    private static final String ALL_FIELDS = " id,projectname,moduleName,versionNumber,metricType,metricValue ";
    private static final String SELECT_BY_PROJECT = "SELECT " + ALL_FIELDS + " FROM MetricValue where projectName=? AND moduleName=?";
    private static final String SELECT_BY_PROJECT_AND_VERSION = "SELECT " + ALL_FIELDS + " FROM MetricValue where projectName=? AND moduleName=? AND versionNumber=?";
    private static final String INSERT_QUERY = "insert into MetricValue  values (null,?,?,?,?,?)";
    private static final String CREATE_TABLE_SCRIPT = "CREATE TABLE IF NOT EXISTS MetricValue (id bigint not null auto_increment primary key, projectName varchar(100) not null, moduleName varchar(100) not null, versionNumber bigint not null, metricType varchar(10) not null, metricValue bigint not null)";
    /**
     * список имен классов-драйверов БД
     */
    private static final String[] driverClassNames = {"com.mysql.jdbc.Driver","org.h2.Driver"};
    /**
     * Соединение с БД, используйте getConnection() для доступа к этому полю.
     * Нельзя использовать значение этого поля непосредственно - оно равно null после ошибок работы с БД.
     */
    private Connection connection;
    private Collection<DatabaseStateListener> stateListeners = new ArrayList<DatabaseStateListener>();

    public DataBaseManager(Configuration configuration) {
        beanListHandler = new BeanListHandler<Metric>(Metric.class);
        queryRunner = new QueryRunner();
        loadDrivers();
        this.configuration = configuration;
        configuration.addEventListener(new ConfigurationChangeListener() {
            @Override
            public void connectionStringChanged() {
                disconnect();
            }
        });
    }

    /**
     * Загрузка всех классов-драйверов БД, перечисленных в {@link #driverClassNames}
     */
    private void loadDrivers() {
        for (String className : driverClassNames) {
            try {
                Class.forName(className);
                log.debug("загружен класс драйвера БД: " + className);
            } catch (ClassNotFoundException e) {
                log.warn("класс драйвера БД не найден", e);
            }
        }
    }

    /**
     * Получение соединения с базой данных.
     * Выполняется ленивая инициализация поля connection (если оно равно null, то создается новое соединение, иначе
     * возвращается старое соединение).
     *
     * @return соединение с БД
     * @throws SQLException при ошибках соединения с БД.
     */
    private Connection getConnection() throws SQLException {
        if(connection == null) {
            try {
                connection = DriverManager.getConnection(configuration.getConnectionString());
            } catch (SQLException exception) {
                log.error("ошибка соединения с сервером БД", exception);
                throw exception;
            }
            log.info("соединение с БД установлено");
        }
        return connection;
    }

    /**
     * Выбирает данные из базы по значениям следующих параметров
     *
     * @param projectName имя проекта
     * @param moduleName  имя модуля
     *
     * @return возвращает список Metric для проекта projectName
     */
    public List<Metric> selectVersion(String projectName, String moduleName) {
        List<Metric> resultList = new ArrayList<Metric>();
        try {
            resultList = queryRunner.query(getConnection(), SELECT_BY_PROJECT, beanListHandler, projectName, moduleName);
            log.debug("выборка метрик из БД для проекта " + projectName + ", модуля " + moduleName);
            log.debug("результат: " + resultList);
            reportSuccess("Выборка версий для модуля " + moduleName + " произведена успешно");
        } catch (SQLException e) {
            log.error("ошибка выборки метрик из БД для проекта " + projectName + ", модуля " + moduleName, e);
            disconnect();
            reportError("Ошибка при выборке версий (" + e.toString() + ")");
        }
        return resultList;
    }

    /**
     * Выбирает данные из базы по значениям следующих параметров
     *
     * @param projectName имя проекта
     * @param moduleName  имя модуля
     * @param version     номер версии
     *
     * @return возвращает список Metric для проекта projectName
     */
    public List<Metric> selectByNameAndVersion(String projectName, String moduleName, Long version) {
        List<Metric> resultList = new ArrayList<Metric>();
        try {
            resultList = queryRunner
                    .query(getConnection(), SELECT_BY_PROJECT_AND_VERSION, beanListHandler, projectName, moduleName, version);
            log.debug("выборка метрик из БД для проекта " + projectName + ", модуля " + moduleName + ", версии " + version);
            log.debug("результат: " + resultList);
            reportSuccess("Выборка метрик произведена успешно");
        } catch (SQLException e) {
            log.error(MessageFormat.format("ошибка выборки метрик из БД для проекта {0}, модуля {1}, версии {2}", projectName, moduleName,
                    version), e);
            disconnect();
            reportError("Ошибка при выборке метрик (" + e.toString() + ")");
        }
        return resultList;
    }

    /**
     * вставляет в базу данные
     *
     * @param projectName   имя проетка
     * @param moduleName    имя модуля
     * @param versionNumber версия
     * @param metricType    тип метрики
     * @param metricValue   значение метрики
     */
    public void insertData(String projectName, String moduleName, long versionNumber, String metricType, long metricValue) {
        try {
            queryRunner.update(getConnection(), INSERT_QUERY, projectName, moduleName, versionNumber, metricType, metricValue);
            log.debug(MessageFormat.format("вставка в БД : проект {0}, модуль {1}, версия {2}, метрика {3}, значение {4}", projectName,
                    moduleName, versionNumber, metricType, metricValue));
            reportSuccess("Добавление данных произведено успешно");
        } catch (SQLException e) {
            log.error(
                    MessageFormat.format("ошибка вставки в БД : проект {0}, модуль {1}, версия {2}, метрика {3}, значение {4}", projectName,
                            moduleName, versionNumber, metricType, metricValue), e);
            disconnect();
            reportError("Ошибка при добавлении данных (" + e.toString() + ")");
        }
    }

    /**
     * Добавить слушатель состояния менеджера БД
     *
     * @param listener слушатель
     */
    public void addStateListener(DatabaseStateListener listener) {
        stateListeners.add(listener);
    }

    /**
     * Уведомить всех слушателей состояния об успешной операции, отправив им указанное сообщение
     *
     * @param message сообшение
     */
    private void reportSuccess(String message) {
        for (DatabaseStateListener listener : stateListeners) {
            listener.notifySuccess(message);
        }
    }

    /**
     * Уведомить всех слушателей состояния об ошибке при выполнении операции,
     * отправив им указанное сообщение
     *
     * @param message сообшение
     */
    private void reportError(String message) {
        for (DatabaseStateListener listener : stateListeners) {
            listener.notifyError(message);
        }
    }

    /**
     * Разорвать соединение с БД.
     */
    public void disconnect() {
        if(connection != null) {
            try {
                connection.close();
                log.info("соединение с БД закрыто");
            } catch (SQLException e) {
                log.warn("ошибка при закрытии соединения с БД", e);
            } finally {
                connection = null;
            }
        }
    }

    /**
     * Создать таблицу в БД.
     */
    public void createTables() {
        try {
            getConnection().createStatement().execute(CREATE_TABLE_SCRIPT);
        } catch (SQLException e) {
            log.error("ошибка при создании таблиц", e);
            disconnect();
            reportError("Ошибка при создании таблиц");
        }
    }
}
